package asteroids.model;
import asteroids.CollisionListener;
import asteroids.model.programs.Program;
import be.kuleuven.cs.som.annotate.*;


/**
 * 
 * @invar	  The coordinates of each ship must be valid.
 * 			| isValidCoordinate(getXCoordinate) &&
 * 			| isValidCoordinate(getYCoordinate)
 * @invar     The velocity according to the x and y axis must be valid.
 * 			| isValidVelocity(getXVelocity()) && isValidVelocity(getYVelocity)		
 * @invar 	  The total velocity of each ship is less than maxSpeeded().
 * 		 	| isValidVelocity((Math.sqrt(Math.pow(xVelocity, 2)+Math.pow(yVelocity,2))))
 * @invar 	  The radius of each ship is valid.
 * 		 	| isValidRadius(getRadius());
 * 
 * @version 0.3
 * @author  Tandan Sunil, Van Deun Luca
 *
 */
public class Ship extends Entity{

	/**
	 * Initialize this new ship with given coordinates and speed according to x- and y- axes, radius and direction.
	 *  
	 * @param  xCoordinate 
	 * 		   The x-coordinate for this new ship in kilometers.
	 * @param  yCoordinate
	 * 		   The y-coordinate for this new ship in kilometers.
	 * @param  xVelocity
	 * 		   The initial velocity for this new ship according to the x-axis in kilometers/second.
	 * @param  yVelocity
	 * 	       The initial velocity for this new ship according to the y-axis in kilometers/second.
	 * @param  radius
	 * 		   The radius for this new circular ship in kilometers. 
	 * @param  angle
	 * 		   The initial direction for this new ship in radians (0 = east, Pi/2 = north, Pi = west, 3*Pi/2 = south).
	 * @param  mass
	 * 		   The mass of this new ship in kilogram.		   
	 * @pre	   The given radius must be a valid radius for a ship.
	 * 		 | isValidRadius(radius)
	 * @pre	   The total speed of this new ship must be valid.
	 * 	 	 | isValidVelocity((Math.sqrt(Math.pow(xVelocity, 2)+Math.pow(yVelocity,2)))) 
	 * @pre    The angle of this new ship must be valid.
	 * 		 | isValidAngle(angle)
	 * @post   The new ship has the given x-coordinate.
	 * 		 | new.getXCoordinate() == xCoordinate
	 * @post   The new ship has the given y-coordinate.
	 * 		 | new.getYCoordinate() == yCoordinate
	 * @post   The new ship has the given radius.
	 * 		 | new.getRadius() == radius
	 * @post   The new ship has the given angle.
	 * 		 | new.getAngle() == angle
	 * @post   The new ship has the given mass.
	 * 		 | new.getMass() == mass
	 * @effect The given velocities are set as the new velocities of this new ship.
	 * 		 | setVelocity(xVelocity,yVelocity)
	 * @throws IllegalArgumentException
	 * 		   The given radius is invalid.
	 * 		 | ! isValidRadius(radius)
	 * @throws IllegalArgumentException
	 * 		   The given coordinate is invalid.
	 * 		 | ! isValidCoordinate(xCoordinate) || ! isValidCoordinate(yCoordinate)
	 */
	public Ship(double xCoordinate, double yCoordinate, double xVelocity,
			double yVelocity, double radius, double angle, double mass) throws IllegalArgumentException{
		super(xCoordinate, yCoordinate, xVelocity, yVelocity, radius, mass);	
			setAngle(angle);
			thruster = false;
	
	}
	
	/**
	 * Initialize this new ship with given coordinates and speed according to x- and y- axes, radius, direction,mass and maximum speed.
	 *  
	 * @param  xCoordinate 
	 * 		   The x-coordinate for this new ship in kilometers.
	 * @param  yCoordinate
	 * 		   The y-coordinate for this new ship in kilometers.
	 * @param  xVelocity
	 * 		   The initial velocity for this new ship according to the x-axis in kilometers/second.
	 * @param  yVelocity
	 * 	       The initial velocity for this new ship according to the y-axis in kilometers/second.
	 * @param  radius
	 * 		   The radius for this new circular ship in kilometers. 
	 * @param  angle
	 * 		   The initial direction for this new ship in radians (0 = east, Pi/2 = north, Pi = west, 3*Pi/2 = south). 
	 * @param  maxSpeed
	 * 		   The maximum speed for this new ship.
	 * @param  mass
	 * 		   The mass of this new ship in kilogram.
	 * @pre	   The given radius must be a valid radius for a ship.
	 * 		 | isValidRadius(radius)
	 * @pre	   The total speed of this new ship must be valid.
	 * 	 	 | isValidVelocity((Math.sqrt(Math.pow(xVelocity, 2)+Math.pow(yVelocity,2)))) 
	 * @pre    The angle of this new ship must be valid.
	 * 		 | isValidAngle(angle)
	 * @post   The new ship has the given x-coordinate.
	 * 		 | new.getXCoordinate() == xCoordinate
	 * @post   The new ship has the given y-coordinate.
	 * 		 | new.getYCoordinate() == yCoordinate
	 * @post   The new ship has the given radius.
	 * 		 | new.getRadius() == radius
	 * @post   The new ship has the given angle.
	 * 		 | new.getAngle() == angle
	 * @post   The new ship has the given mass.
	 * 		 | new.getMass() == mass
	 * @post   The getMaxSpeed() of the new ship is equal to the given maxSpeed if it is valid.
	 * 		 | if(maxSpeed > 0 && maxSpeed <= SPEED_OF_LIGHT)
	 * 		 |		then new.getMaxSpeed() == maxSpeed
	 * @post   If maxSpeed is greater than the SPEED_OF_LIGHT, or smaller than or equal to 0, or not a number 
	 * 		   then getMaxSpeed() is equal to SPEED_OF_LIGHT.
	 * 		 | if (maxSpeed > SPEED_OF_LIGHT || maxSpeed <= 0 || Double.isNaN(maxSpeed))
	 * 		 |		then new.getMaxSpeed() == SPEED_OF_LIGHT
	 * @effect The given velocities are set as the new velocities of this new ship.
	 * 		 | setVelocity(xVelocity,yVelocity)
	 * @throws IllegalArgumentException
	 * 		   The given radius is invalid.
	 * 		 | ! isValidRadius(radius)
	 * @throws IllegalArgumentException
	 * 		   The given coordinate is invalid.
	 * 		 | ! isValidCoordinate(xCoordinate) || ! isValidCoordinate(yCoordinate)
	 */
	public Ship(double xCoordinate, double yCoordinate, double xVelocity,
			double yVelocity, double radius, double angle,double mass,double maxSpeed) throws IllegalArgumentException{
			super(xCoordinate,yCoordinate,xVelocity,yVelocity,radius,mass,maxSpeed);
	}
	
	
	///////////
	// ANGLE //
	///////////
	
	private double angle;
	
	/**
	 * Returns the angle of this ship in radians.
	 *
	 */
	@Basic
	public double getAngle() {
		return angle;
	}
	
	/**
	 * Sets the angle of this ship to the given value.
	 * 
	 * @param 	 angle
	 * 			 The new angle of this ship.
	 * @pre		 The angle must be greater than or equal to 0 and less than 2*Math.PI.	
	 *			| (angle >= 0  && angle < 2*Math.PI)
	 * @post      The new angle of this ship is equal to the given 
	 *			| (new this).getAngle = angle
	 */			
	public void setAngle(double angle) {
		assert(angle >= 0  && angle < 2*Math.PI);
		this.angle = angle;
	}
	
	/**
	 * Changes the direction of this ship by adding the given angle to the current angle.
	 * 
	 * @param 	  angle
	 * 			  The angle that has to be added to the current angle.
	 * @pre 	  The given angle must be greater than 0 and less than 2*Math.PI
	 *			| (angle > 0 && angle < 2* Math*PI ) 
	 * @post	  The new direction of this ship is equal to the old direction plus the given angle.
	 * 			  If old angle plus the given angle exceed 2*Pi then the total angle is reduced to total angle modulo 2*Pi.
	 *			| (new this).getAngle() = (getAngle() + angle) % (2*Math.PI)
	 * 
	 */			
	public void addAngle(double angle){
		assert (angle > 0 && angle < 2*Math.PI);
		setAngle( (getAngle() + angle) % (2*Math.PI) );
	}
	
	
	
	
	
	//////////////
	// MOVEMENT //
	//////////////
	
	private boolean thruster;
	
	/**
	 * 
	 * Changes the velocities of this ship according to the given amount taking this ship's properties 
	 * into account.
	 * 
	 * @param 	  amount
	 * 			  The amount that determines the change of speed
	 * @effect    If the given amount is negative then the amount is set to 0 and if the amount
	 * 			  getMaxSpeed() then it is set to getMaxSpeed(). The new velocities are then calculated 
	 * 			  using the (converted) amount, current velocities and the current angle of the ship. 
	 * 			| 	if(Double.isNaN(amount))
	 *			|		then amount = 0;
	 *			|	else if(amount < 0 || amount == Double.NEGATIVE_INFINITY || amount == Double.MIN_VALUE)
	 *			|		then amount = 0;
	 *			|	else if (amount == Double.POSITIVE_INFINITY || amount == Double.MAX_VALUE || amount > getMaxSpeed())
	 *			|		then amount = getMaxSpeed();
	 *			|	setVelocity(this.getXVelocity() + amount * Math.cos(this.getAngle()), this.getYVelocity() + amount * Math.sin(this.getAngle()));
	 * 			
	 */	
	public void thrust(double amount){
		if(Double.isNaN(amount)){
			amount = 0;
		}
		else if(amount < 0 || amount == Double.NEGATIVE_INFINITY || amount == Double.MIN_VALUE)
			amount = 0;
		else if (amount == Double.POSITIVE_INFINITY || amount == Double.MAX_VALUE || amount > getMaxSpeed())
			amount = getMaxSpeed();
		double newXVelocity = this.getXVelocity() + amount * Math.cos(this.getAngle());
		double newYVelocity = this.getYVelocity() + amount * Math.sin(this.getAngle());
		this.setVelocity(newXVelocity, newYVelocity);
		
	}
	
	/**
	 * Updates the velocity of this ship according to the current speed and thruster force.
	 * 
	 * @effect	Calls the thrust method with appropriate amount if the thruster is active.
	 *			|if(isShipThrusterActive())
	 *			| then thrust(getThrusterForce()/getMass()*time)
	 */
	public void updateVelocity(double time){
		if(isShipThrusterActive())
		thrust(getThrusterForce()/getMass()*time/1000);
	}
	
	private  double thrusterForce = 1.1*Math.pow(10,21); //in N/s
	
	/**
	 * Returns the thruster force of this ship.
	 * 
	 */
	@Basic
	public double getThrusterForce(){
		return thrusterForce;
	}
	/**
	 * Sets the thruster force of this ship.
	 * @param 	force
	 * 			The force to be set.
	 * @post	...
	 *			| new.getThrusterForce()== force
	 */	
	public void setThrusterForce(double force){
		assert(isValidForce(force));
		thrusterForce = force;
	}
	
	/**
	 * 
	 * @param 	force
	 * 			The force to be checked.
	 * @return	...
	 * 			|result == (Double.isNaN(force) || Double.isInfinite(force) ||force <= 0)
	 * 
	 */
	public static boolean isValidForce(double force){
		if(Double.isNaN(force) || Double.isInfinite(force) ||force <= 0)
			return false;
		return true;
	}
	/**
	 * Returns the thruster state of this ship. 
	 *
	 */
	@Basic
	public boolean isShipThrusterActive(){
		return thruster;
	}
	
	/**
	 * Sets the state of thruster to the given boolean.
	 * @param 	active
	 * 			The new state of the thruster of this ship.
	 * @post	The new thruster is equal to the given boolean.
	 * 			| (new this).isShipThrusterActive == active	
	 */
	public void setThruster(boolean active){
		thruster = active;
	}



	//////////////
	// MOVEMENT //
	//////////////
	
	/**
	* Changes the position of this entity based on the current position, velocity and a given time duration.
	* 
	* @param 	duration 
	* 			The duration for which the entity is in movement.
	* @throws 	IllegalArgumentException
	* 			The given duration is invalid.
	* 		   | ! isValidDuration (duration)	
	*/
	@Override
	public void move(double duration)throws IllegalArgumentException{
		
		if(! isValidDuration(duration))
			throw new IllegalArgumentException("Invalid duration");
		
		setCoordinates(getXVelocity()*duration + getXCoordinate(),(getYVelocity()*duration + getYCoordinate()));
		if(isShipThrusterActive()){
			updateVelocity(duration);
		}
	}
	
	/**
	 * Returns a new Bullet with this ship as its source.
	 */
	public Bullet fire(){
		if(getWorld().getNbOfBullets(this)<getBulletLimit())
			return new Bullet(this);
		return null;
	}
	
	private final int bulletLimit =3;
	
	/**
	 * Returns the bullet limit of this ship.
	 */
	@Basic @Immutable
	public int getBulletLimit(){
		return bulletLimit;
	}
	
	
	
	////////////////////////////////////////////////////////////////////////////
	//								Collision   							  //
	////////////////////////////////////////////////////////////////////////////
	
	
	/**
	 * Checks whether the given entity is related to this ship.
	 * 
	 * @param	entity1
	 * 			The entity with which it has to be checked.
	 * @pre  	The given entity is valid.
	 * 			| Entity.isValidEntity(entity1)
	 * @return Returns true only if the given entity is instance of bullet and its source is this ship.
	 * 		|  if(entity instanceof Bullet)
	 * 		|	then result == ((Bullet)entity).getSource() == this
	 * 		|  else result == false
	 */
	public boolean isRelated(Entity entity){
		assert(Entity.isValidEntity(entity));
		if(entity instanceof Bullet)
			return ((Bullet)entity).getSource() == this;
		return false;
	}

	/**
	 * Checks whether this ship can bounce with the given entity.	 
	 * @param	entity1
	 * 			The entity with which it has to be checked.
	 * @pre  	The given entity is valid.
	 * 			| Entity.isValidEntity(entity1)
	 * @return  If the given entity is instance of ship then it is true else entity1.canBounceWith(this).
	 * 			| if(entity1 instanceof Ship) then result== true
	 * 			| else result == entity1.canBounceWith(this)
	 */
	@Override
	public boolean canBounceWith(Entity entity1) {
		assert(Entity.isValidEntity(entity1));
		if(entity1 instanceof Ship)
			return true;
		return entity1.canBounceWith(this);
	}
	
	/**
	 * Resolves the collision between this entity and the given entity.
	 * @param	 entity2
	 * 			| The entity with with the collision occurs.
	 * @param 	collisionListener
	 * 			|  The collision listener that has to be called.
	 * @effect   If this canBounceWith(entity2) then collideBounce(entity2)
	 * 			|if(canBounceWith(entity2))
	 * 			| then this.collideBounce(entity2)
	 * 			| else other.collide(this,collisionListener)
	 * 
	 */
	@Override
	public void collide(Entity other, CollisionListener collisionListener) {
		assert(Entity.isValidEntity(other));
		if(canBounceWith(other))
			collideBounce(other);
		else other.collide(this, collisionListener);		
	}
	
	private Program program;
	
	/**
	 * Sets the program for this ship.
	 * @param program
	 *			The program to be set.
	 * @post  If the program is valid getProgram() gives the given program.
	 * 		  | if(isValidProgram(program))
	 * 		  |   then new.getProgram() = program
	 * @throws	IllegalArgumentException
	 * 		  | !isValidProgram(program)	
	 */
	@Basic
	public void setProgram(Program program)throws IllegalArgumentException{
		if(!isValidProgram(program))
			throw new IllegalArgumentException("Got null as program");
		this.program=program;
		program.setOwner(this);
	}
	
	/**
	 * Checks whether the given program is valid.
	 * @param program
	 * 			The program to be checked.
	 * @return	Returns true if program is not null.
	 * 			result == program !=null
	 */
	public static boolean isValidProgram(Program program){
		return program!=null;	
	}
	/**
	 * Returns the program of this ship if any.
	 */
	@Basic
	public Program getProgram(){
		return program;
	}
	
	/**
	 * Returns whether this ship contains any program.
	 * @return  Returns true if getProgram() != null.
	 * 			| result == getProgram() != null
	 * 		
	 */
	public boolean containsProgram(){
		return getProgram() != null;
	}
	
	/**
	 * Executes the program of this ship.
	 * @effect  Gets the program of this ship and calls the method execute on the program.
	 * 			| getProgram().execute()
	 */
	public void executeProgram(){
		if(containsProgram()){
			getProgram().execute();
			}
			
	}
		
	
}




